var entityMap = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': "&quot;",
  "'": "&#39;",
  "/": "&#x2F;",
  "`": "&#x60;",
  "=": "&#x3D;",
};

function escapeHtml(string) {
  return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) {
    return entityMap[s];
  });
}

function escapeRegExp(string) {
  return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}

function hasProperty(obj, propName) {
  return obj != null && typeof obj === "object" && propName in obj;
}

function parseTemplate(template) {
  const tokens = [];
  let openingTagRe;
  let closingTagRe;
  let closingCurlyTagRe;
  const scanner = new Scanner(template);
  let value;
  let type;
  let start;

  let spaces = [];
  let nonSpace = false;
  let hasTag = false;
  let temp = 0;

  function stripeSpaces() {
    if (hasTag && !nonSpace) {
      while (spaces.length) {
        tokens[spaces.pop()] = undefined;
      }
    } else {
      spaces = [];
    }
    hasTag = false;
    nonSpace = false;
  }

  function compileTags(tagsToCompile) {
    if (typeof tagsToCompile === "string") {
      tagsToCompile = tagsToCompile.split(/\s+/, 2);
    }
    if (!Array.isArray(tagsToCompile) || tagsToCompile.length !== 2) {
      throw new Error("不合法的标签：" + tagsToCompile);
    }
    openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + "\\s*");
    closingTagRe = new RegExp("\\s*" + escapeRegExp(tagsToCompile[1]));
    closingCurlyTagRe = new RegExp(
      "\\s*" + escapeRegExp("}" + tagsToCompile[1])
    );
  }

  compileTags(["{{", "}}"]);

  while (temp < 1000 && !scanner.eos()) {
    temp++;
    start = scanner.pos;
    value = scanner.scanUntil(openingTagRe);
    for (const chr of value) {
      if (/\s/.test(chr)) {
        spaces.push(tokens.length);
      } else {
        nonSpace = true;
      }
      tokens.push(["text", chr, start, ++start]);
      if (chr === "\n") {
        stripeSpaces();
      }
    }
    if (!scanner.scan(openingTagRe)) {
      console.log("没了?");
      break;
    }
    hasTag = true;
    type = scanner.scan(/\{|#|\/|\^|!|>|=/) || "name";
    // 跳过空格
    scanner.scan(/\s*/);
    if (type === "=") {
      value = scanner.scanUntil(/\s*=/);
      scanner.scan(/\s*=/);
      scanner.scanUntil(closingTagRe);
    } else if (type === "{") {
      value = scanner.scanUntil(closingCurlyTagRe);
      scanner.scan(/\}/);
    } else {
      value = scanner.scanUntil(closingTagRe);
    }
    if (!scanner.scan(closingTagRe)) {
      throw new Error("没有闭合标签");
    }
    tokens.push([type, value, start, scanner.pos]);
    if (type === "{" || type === "name") {
      nonSpace = true;
    } else if (type === "=") {
      compileTags(value);
    }
  }
  stripeSpaces();
  return nestTokens(squashTokens(tokens));
}

function squashTokens(tokens) {
  const squashedTokens = [];
  let lastToken;
  for (const token of tokens) {
    if (token) {
      if (token[0] === "text" && lastToken && lastToken[0] === "text") {
        lastToken[1] += token[1];
        lastToken[3] = token[3];
      } else {
        lastToken = token;
        squashedTokens.push(token);
      }
    }
  }
  return squashedTokens;
}

function nestTokens(tokens) {
  const nestedTokens = [];
  let collector = nestedTokens;
  let sections = [];

  for (const token of tokens) {
    switch (token[0]) {
      case "#":
      case "^":
        collector.push(token);
        sections.push(token);
        collector = token[4] = [];
        break;
      case "/":
        const section = sections.pop();
        section[5] = token[2];
        if (sections.length) {
          collector = sections[sections.length - 1][4];
        } else {
          collector = nestedTokens;
        }
        break;
      default:
        collector.push(token);
    }
  }
  return nestedTokens;
}

class Scanner {
  constructor(template) {
    this.string = template;
    this.tail = template;
    this.pos = 0;
  }
  eos() {
    return this.tail === "";
  }
  scan(reg) {
    const match = this.tail.match(reg);
    let matchedValue = "";
    if (match && match.index === 0) {
      matchedValue = match[0];
      this.tail = this.tail.substring(matchedValue.length);
      this.pos += matchedValue.length;
    }
    return matchedValue;
  }
  scanUntil(reg) {
    const index = this.tail.search(reg);
    let value;
    if (index > 0) {
      value = this.tail.substring(0, index);
      this.tail = this.tail.substring(index);
      this.pos += index;
    } else if (index === 0) {
      value = "";
    } else {
      value = this.tail;
      this.tail = "";
      this.pos = this.string.length;
    }
    return value;
  }
}

class Context {
  constructor(view, parentContext) {
    this.cache = {
      ".": view,
    };
    this.view = view;
    this.parent = parentContext;
  }
  push(value) {
    return new Context(value, this);
  }
  lookup(name) {
    let value;
    if (this.cache.hasOwnProperty(name)) {
      value = this.cache[name];
    } else {
      let context = this;
      // true表示找到了name在context.view上的值
      let lookupHit;
      let index;
      let splitName;
      while (context) {
        if (name.includes(".")) {
          value = context.view;
          const names = name.split(".");
          index = 0;
          while (value != null && index < names.length) {
            splitName = names[index];
            if (index === names.length - 1) {
              // 这里表示最后一个splitName属性的值也找到了
              // 也就是说前面的splitName属性对应的值都是有的
              lookupHit = hasProperty(value, splitName);
            }
            value = value[splitName];
            index++;
          }
        } else {
          lookupHit = hasProperty(context.view, name);
          value = context.view[name];
        }
        if (lookupHit) {
          break;
        }
        context = context.parent;
      }
      this.cache[name] = value;
    }
    if (typeof value === "function") {
      value = value.call(this.view);
    }

    return value;
  }
}

class Writer {
  render(template, view, partials) {
    const tokens = parseTemplate(template);
    console.log(tokens);
    const context = view instanceof Context ? view : new Context(view);
    return this.renderTokens(tokens, context, partials, template);
  }
  renderTokens(tokens, context, partials, originalTemplate) {
    let buffer = "";
    let symbol;
    let value;
    for (const token of tokens) {
      value = undefined;
      symbol = token[0];
      if (symbol === "name") {
        value = escapeHtml(context.lookup(token[1]));
      } else if (symbol === "text") {
        value = token[1];
      } else if (symbol === "{") {
        value = context.lookup(token[1]);
      } else if (symbol === "#") {
        value = this.renderSection(token, context, partials, originalTemplate);
      } else if (symbol === "^") {
        value = this.renderInverted(token, context, partials, originalTemplate);
      } else if (symbol === ">") {
        value = this.renderPartial(token, context, partials);
      }
      if (value != null) {
        buffer += value;
      }
    }
    return buffer;
  }
  renderSection(token, context, partials, originalTemplate) {
    let value = context.lookup(token[1]);
    let buffer = "";
    if (!value) {
      return;
    }
    const self = this;
    const subRender = function (text) {
      return self.render(text, context, partials);
    };
    if (Array.isArray(value)) {
      for (const item of value) {
        buffer += this.renderTokens(
          token[4],
          context.push(item),
          partials,
          originalTemplate
        );
      }
    } else if (typeof value === "function") {
      buffer = value.call(
        context.view,
        originalTemplate.substring(token[3], token[5]),
        subRender
      );
    } else if (typeof value === "object") {
      buffer = this.renderTokens(
        token[4],
        context.push(value),
        partials,
        originalTemplate
      );
    } else {
      buffer = this.renderTokens(token[4], context, partials, originalTemplate);
    }
    return buffer;
  }
  renderInverted(token, context, partials, originalTemplate) {
    let value = context.lookup(token[1]);
    if (!value || (Array.isArray(value) && value.length === 0)) {
      return this.renderTokens(token[4], context, partials, originalTemplate);
    }
  }
  renderPartial(token, context, partials) {
    let value = partials[token[1]];
    if (value) {
      const tokens = parseTemplate(value);
      return this.renderTokens(tokens, context, partials, value);
    }
  }
}

const defaultWriter = new Writer();

const mustache = {
  render(template, view, partials) {
    return defaultWriter.render(template, view, partials);
  },
};

export default mustache;
